/**
 * 
 */
package com.et114.modules.extend.acegi;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Properties;

import org.acegisecurity.AbstractAuthenticationManager;
import org.acegisecurity.AccountExpiredException;
import org.acegisecurity.AcegiMessageSource;
import org.acegisecurity.Authentication;
import org.acegisecurity.AuthenticationException;
import org.acegisecurity.AuthenticationServiceException;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.CredentialsExpiredException;
import org.acegisecurity.DisabledException;
import org.acegisecurity.LockedException;
import org.acegisecurity.concurrent.ConcurrentLoginException;
import org.acegisecurity.concurrent.ConcurrentSessionController;
import org.acegisecurity.concurrent.NullConcurrentSessionController;
import org.acegisecurity.event.authentication.AbstractAuthenticationEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureBadCredentialsEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureConcurrentLoginEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureCredentialsExpiredEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureDisabledEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureExpiredEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureLockedEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureProviderNotFoundEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureProxyUntrustedEvent;
import org.acegisecurity.event.authentication.AuthenticationFailureServiceExceptionEvent;
import org.acegisecurity.event.authentication.AuthenticationSuccessEvent;
import org.acegisecurity.providers.AbstractAuthenticationToken;
import org.acegisecurity.providers.AuthenticationProvider;
import org.acegisecurity.providers.ProviderManager;
import org.acegisecurity.providers.ProviderNotFoundException;
import org.acegisecurity.providers.cas.ProxyUntrustedException;
import org.acegisecurity.userdetails.UsernameNotFoundException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.util.Assert;

import com.et114.modules.org.vo.UserVo;

/**
 * @author guanhw
 */
public class AwCasProviderManager extends AbstractAuthenticationManager implements
		InitializingBean , ApplicationEventPublisherAware , MessageSourceAware {
	// ~ Static fields/initializers
	// =====================================================================================
	
	private static final Log			logger						= LogFactory.getLog ( ProviderManager.class );
	private static final Properties		DEFAULT_EXCEPTION_MAPPINGS	= new Properties ( );
	private AwCasAuthenticationProvider   authenticationProvider ; 
	// ~ Instance fields
	// ================================================================================================
	
	private ApplicationEventPublisher	applicationEventPublisher;
	private ConcurrentSessionController	sessionController			= new NullConcurrentSessionController ( );
	//private List						providers;
	protected MessageSourceAccessor		messages					= AcegiMessageSource.getAccessor ( );
	private Properties					exceptionMappings			= new Properties ( );
	private Properties					additionalExceptionMappings	= new Properties ( );
	
	//DTO
	private AwBean						bean;
	private UserVo                      currentUser ;
	
	static {
		DEFAULT_EXCEPTION_MAPPINGS.put ( AccountExpiredException.class
				.getName ( ) , AuthenticationFailureExpiredEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( AuthenticationServiceException.class
				.getName ( ) , AuthenticationFailureServiceExceptionEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( LockedException.class.getName ( ) ,
				AuthenticationFailureLockedEvent.class.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( CredentialsExpiredException.class
				.getName ( ) ,
				AuthenticationFailureCredentialsExpiredEvent.class.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( DisabledException.class.getName ( ) ,
				AuthenticationFailureDisabledEvent.class.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( BadCredentialsException.class
				.getName ( ) , AuthenticationFailureBadCredentialsEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( UsernameNotFoundException.class
				.getName ( ) , AuthenticationFailureBadCredentialsEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( ConcurrentLoginException.class
				.getName ( ) , AuthenticationFailureConcurrentLoginEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( ProviderNotFoundException.class
				.getName ( ) , AuthenticationFailureProviderNotFoundEvent.class
				.getName ( ) );
		DEFAULT_EXCEPTION_MAPPINGS.put ( ProxyUntrustedException.class
				.getName ( ) , AuthenticationFailureProxyUntrustedEvent.class
				.getName ( ) );
	}
	
	public AwCasProviderManager ( ) {
		exceptionMappings.putAll ( DEFAULT_EXCEPTION_MAPPINGS );
	}
	
	// ~ Methods
	// ========================================================================================================
	
	public void afterPropertiesSet ( ) throws Exception {
		Assert.notNull ( authenticationProvider , "authenticationProvider must be set" ) ; 
		Assert.isInstanceOf ( AuthenticationProvider.class , authenticationProvider , "Can only provide AuthenticationProvider instances" );		
		Assert.notNull ( messages , "A message source must be set" );
		exceptionMappings.putAll ( additionalExceptionMappings );
		doAddExtraDefaultExceptionMappings ( exceptionMappings );
	}
	
	
	/**
	 * Provided so subclasses can add extra exception mappings during startup if
	 * no exception mappings are injected by the IoC container.
	 * 
	 * @param exceptionMappings
	 *            the properties object, which already has entries in it
	 * @deprecated This method has been removed from the 2.0 series; please use
	 *             the {@link #additionalExceptionMappings} property instead to
	 *             inject additional exception to event mappings
	 */
	protected void doAddExtraDefaultExceptionMappings (
			Properties exceptionMappings ) {
	}
	
	/**
	 * Attempts to authenticate the passed {@link Authentication} object.
	 * <p>
	 * The list of {@link AuthenticationProvider}s will be successively tried
	 * until an <code>AuthenticationProvider</code> indicates it is capable of
	 * authenticating the type of <code>Authentication</code> object passed.
	 * Authentication will then be attempted with that
	 * <code>AuthenticationProvider</code>.
	 * </p>
	 * <p>
	 * If more than one <code>AuthenticationProvider</code> supports the passed
	 * <code>Authentication</code> object, only the first
	 * <code>AuthenticationProvider</code> tried will determine the result. No
	 * subsequent <code>AuthenticationProvider</code>s will be tried.
	 * </p>
	 * 
	 * @param authentication
	 *            the authentication request object.
	 * 
	 * @return a fully authenticated object including credentials.
	 * 
	 * @throws AuthenticationException
	 *             if authentication fails.
	 */
	public Authentication doAuthentication ( Authentication authentication )
			throws AuthenticationException {
		Class toTest = authentication.getClass ( );
		AuthenticationException lastException = null;

		AwCasAuthenticationProvider provider = ( AwCasAuthenticationProvider ) this.authenticationProvider ; 
		
			if ( provider.supports ( toTest ) ) {
				logger.info ( "Authentication attempt using " + provider.getClass ( ).getName ( ) );
				Authentication result = null;
				try {
					result = provider.authenticate ( authentication );
					
					copyDetails ( authentication , result );
					sessionController.checkAuthenticationAllowed ( result );
				} catch ( AuthenticationException ae ) {
					lastException = ae;
					result = null;
				}
				
				if ( result != null ) {
					//DTO to filter
					setCurrentUser ( provider.getCurrentUser ( ) ) ; 
					setBean( provider.getBean ( ) ) ; 
					sessionController.registerSuccessfulAuthentication ( result );
					publishEvent ( new AuthenticationSuccessEvent ( result ) );
					return result;
				}
			}
		
		
		if ( lastException == null ) {
			lastException = new ProviderNotFoundException ( messages
					.getMessage ( "ProviderManager.providerNotFound" ,
							new Object [ ] { toTest.getName ( ) } ,
							"No AuthenticationProvider found for {0}" ) );
		}
		
		// Publish the event
		String className = exceptionMappings.getProperty ( lastException
				.getClass ( ).getName ( ) );
		AbstractAuthenticationEvent event = null;
		
		if ( className != null ) {
			try {
				Class clazz = getClass ( ).getClassLoader ( ).loadClass (
						className );
				Constructor constructor = clazz.getConstructor ( new Class [ ] {
						Authentication.class , AuthenticationException.class } );
				Object obj = constructor.newInstance ( new Object [ ] {
						authentication , lastException } );
				Assert.isInstanceOf ( AbstractAuthenticationEvent.class , obj ,
						"Must be an AbstractAuthenticationEvent" );
				event = ( AbstractAuthenticationEvent ) obj;
			} catch ( ClassNotFoundException ignored ) {
			} catch ( NoSuchMethodException ignored ) {
			} catch ( IllegalAccessException ignored ) {
			} catch ( InstantiationException ignored ) {
			} catch ( InvocationTargetException ignored ) {
			}
		}
		
		if ( event != null ) {
			publishEvent ( event );
		} else {
			if ( logger.isDebugEnabled ( ) ) {
				logger.debug ( "No event was found for the exception "
						+ lastException.getClass ( ).getName ( ) );
			}
		}
		
		// Throw the exception
		throw lastException;
	}
	
	/**
	 * Copies the authentication details from a source Authentication object to
	 * a destination one, provided the latter does not already have one set.
	 * 
	 * @param source
	 *            source authentication
	 * @param dest
	 *            the destination authentication object
	 */
	private void copyDetails ( Authentication source , Authentication dest ) {
		if ( ( dest instanceof AbstractAuthenticationToken )
				&& ( dest.getDetails ( ) == null ) ) {
			AbstractAuthenticationToken token = ( AbstractAuthenticationToken ) dest;
			
			token.setDetails ( source.getDetails ( ) );
		}
	}
	

	
	/**
	 * The configured {@link ConcurrentSessionController} is returned or the
	 * {@link NullConcurrentSessionController} if a specific one has not been
	 * set.
	 * 
	 * @return {@link ConcurrentSessionController} instance
	 */
	public ConcurrentSessionController getSessionController ( ) {
		return sessionController;
	}
	
	public void setApplicationEventPublisher (
			ApplicationEventPublisher applicationEventPublisher ) {
		this.applicationEventPublisher = applicationEventPublisher;
	}
	
	public void setMessageSource ( MessageSource messageSource ) {
		this.messages = new MessageSourceAccessor ( messageSource );
	}
	
	/**
	 * Set the {@link ConcurrentSessionController} to be used for limiting
	 * user's sessions. The {@link NullConcurrentSessionController} is used by
	 * default
	 * 
	 * @param sessionController
	 *            {@link ConcurrentSessionController}
	 */
	public void setSessionController (
			ConcurrentSessionController sessionController ) {
		this.sessionController = sessionController;
	}
	
	private void publishEvent ( ApplicationEvent event ) {
		if ( applicationEventPublisher != null ) {
			applicationEventPublisher.publishEvent ( event );
		}
	}
	
	/**
	 * Sets additional exception to event mappings. These are automatically
	 * merged with the default exception to event mappings that
	 * <code>ProviderManager</code> defines.
	 * 
	 * @param additionalExceptionMappings
	 *            where keys are the fully-qualified string name of the
	 *            exception class and the values are the fully-qualified string
	 *            name of the event class to fire
	 */
	public void setAdditionalExceptionMappings (
			Properties additionalExceptionMappings ) {
		this.additionalExceptionMappings = additionalExceptionMappings;
	}

	public AwCasAuthenticationProvider getAuthenticationProvider ( ) {
		return authenticationProvider;
	}

	public void setAuthenticationProvider (
			AwCasAuthenticationProvider authenticationProvider ) {
		this.authenticationProvider = authenticationProvider;
	}

	public AwBean getBean ( ) {
		return bean;
	}

	public void setBean ( AwBean bean ) {
		this.bean = bean;
	}

	public UserVo getCurrentUser ( ) {
		return currentUser;
	}

	public void setCurrentUser ( UserVo currentUser ) {
		this.currentUser = currentUser;
	}

}
